package cn.uncode.baas.server.acl.token;

import cn.uncode.baas.server.constant.Resource;

import java.io.Serializable;
import java.util.*;

/**
 * access token for rest.
 * 
 * @author wj.ye
 */
public class DefaultAccessToken implements Serializable, AccessToken {

	private static final long serialVersionUID = 914967629530462926L;

	private String value;

	private Date expiration;

	private String tokenType = BEARER_TYPE.toLowerCase();

	private Set<String> scope;

	private Map<String, Object> additionalInformation = Collections.emptyMap();

	/**
	 * Create an access token from the value provided.
	 */
	public DefaultAccessToken(String value) {
		this.value = value;
	}
	
	/**
	 * Create an access token from the value provided.
	 */
	public DefaultAccessToken(String bucket, String user, String value, List<String> groups, List<String> roles) {
		this.value = value;
		Map<String, Object> additionalInformation = new HashMap<String, Object>();
		additionalInformation.put(AccessToken.BUCKET, bucket);
		additionalInformation.put(AccessToken.USER, user);
		if(null != groups){
			additionalInformation.put(AccessToken.GROUP, groups);
		}
		if(null != roles){
			additionalInformation.put(AccessToken.ROLE, roles);
		}
		this.setAdditionalInformation(additionalInformation);
		this.setExpiration(new Date(System.currentTimeMillis() + (Resource.ACCESS_TOKEN_EXPIRATION_TIME * 1000L)));
		if(this.scope == null){
			this.scope = new HashSet<String>();
		}
		this.scope.add("BASIC");
	}

	/**
	 * Private constructor for JPA and other serialization tools.
	 */
	@SuppressWarnings("unused")
	private DefaultAccessToken() {
		this((String) null);
	}

	/**
	 * Copy constructor for access token.
	 * 
	 * @param accessToken
	 */
	public DefaultAccessToken(AccessToken accessToken) {
		this(accessToken.getValue());
		setAdditionalInformation(accessToken.getAdditionalInformation());
		setExpiration(accessToken.getExpiration());
		setScope(accessToken.getScope());
		setTokenType(accessToken.getTokenType());
	}

	public DefaultAccessToken setValue(String value) {
		DefaultAccessToken result = new DefaultAccessToken(this);
		result.value = value;
		return result;
	}

	/**
	 * The token value.
	 * 
	 * @return The token value.
	 */
	public String getValue() {
		return value;
	}

	public int getExpiresIn() {
		return expiration != null ? Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
				.intValue() : 0;
	}

	protected void setExpiresIn(int delta) {
		setExpiration(new Date(System.currentTimeMillis() + delta));
	}

	/**
	 * The instant the token expires.
	 * 
	 * @return The instant the token expires.
	 */
	public Date getExpiration() {
		return expiration;
	}

	/**
	 * The instant the token expires.
	 * 
	 * @param expiration The instant the token expires.
	 */
	public void setExpiration(Date expiration) {
		this.expiration = expiration;
	}

	/**
	 * Convenience method for checking expiration
	 * 
	 * @return true if the expiration is befor ethe current time
	 */
	public boolean isExpired() {
		return expiration != null && expiration.before(new Date());
	}

	/**
	 * The token type, as introduced in draft 11 of the OAuth 2 spec. The spec doesn't define (yet) that the valid token
	 * types are, but says it's required so the default will just be "undefined".
	 * 
	 * @return The token type, as introduced in draft 11 of the OAuth 2 spec.
	 */
	public String getTokenType() {
		return tokenType;
	}

	/**
	 * The token type, as introduced in draft 11 of the OAuth 2 spec.
	 * 
	 * @param tokenType The token type, as introduced in draft 11 of the OAuth 2 spec.
	 */
	public void setTokenType(String tokenType) {
		this.tokenType = tokenType;
	}

	/**
	 * The scope of the token.
	 * 
	 * @return The scope of the token.
	 */
	public Set<String> getScope() {
		return scope;
	}

	/**
	 * The scope of the token.
	 * 
	 * @param scope The scope of the token.
	 */
	public void setScope(Set<String> scope) {
		this.scope = scope;
	}

	@Override
	public boolean equals(Object obj) {
		return obj != null && toString().equals(obj.toString());
	}

	@Override
	public int hashCode() {
		return toString().hashCode();
	}

	@Override
	public String toString() {
		return String.valueOf(getValue());
	}

	public static AccessToken valueOf(Map<String, String> tokenParams) {
		DefaultAccessToken token = new DefaultAccessToken(tokenParams.get(ACCESS_TOKEN));

		if (tokenParams.containsKey(EXPIRES_IN)) {
			long expiration = 0;
			try {
				expiration = Long.parseLong(String.valueOf(tokenParams.get(EXPIRES_IN)));
			}
			catch (NumberFormatException e) {
				// fall through...
			}
			token.setExpiration(new Date(System.currentTimeMillis() + (expiration * 1000L)));
		}

		if (tokenParams.containsKey(SCOPE)) {
			Set<String> scope = new TreeSet<String>();
			for (StringTokenizer tokenizer = new StringTokenizer(tokenParams.get(SCOPE), " ,"); tokenizer
					.hasMoreTokens();) {
				scope.add(tokenizer.nextToken());
			}
			token.setScope(scope);
		}

		if (tokenParams.containsKey(TOKEN_TYPE)) {
			token.setTokenType(tokenParams.get(TOKEN_TYPE));
		}

		return token;
	}

	/**
	 * Additional information that token granters would like to add to the token, e.g. to support new token types.
	 * 
	 * @return the additional information (default empty)
	 */
	public Map<String, Object> getAdditionalInformation() {
		return additionalInformation;
	}

	/**
	 * Additional information that token granters would like to add to the token, e.g. to support new token types. If
	 * the values in the map are primitive then remote communication is going to always work. It should also be safe to
	 * use maps (nested if desired), or something that is explicitly serializable by Jackson.
	 * 
	 * @param additionalInformation the additional information to set
	 */
	public void setAdditionalInformation(Map<String, Object> additionalInformation) {
		this.additionalInformation = new LinkedHashMap<String, Object>(additionalInformation);
	}

}
